Skip to content

feat(sum-subop): Spark BIGINT sum AArch64 SVE on HashAgg#563

Open
LeiRui wants to merge 4 commits into
bytedance:mainfrom
LeiRui:pr-sumInt64
Open

feat(sum-subop): Spark BIGINT sum AArch64 SVE on HashAgg#563
LeiRui wants to merge 4 commits into
bytedance:mainfrom
LeiRui:pr-sumInt64

Conversation

@LeiRui

@LeiRui LeiRui commented May 17, 2026

Copy link
Copy Markdown

What problem does this PR solve?

Spark sum(bigint) on HashAgg can spend significant time in per-group scalar updates. This PR adds SumAggregateSparkInt64SubOp as the default aggregate for non-decimal spark_sum(bigint) → bigint on Linux aarch64 with 256-bit SVE (svcntb()==32), providing batch SVE updates when Spark overflow checking is enabled. The fast path supports flat, constant, and dictionary inputs. Other platforms or SVE widths use original SumAggregateBase. Disable SubOp with BOLT_SPARK_SUM_INT64_USE_SUBOP=0.

Issue Number: N/A

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 🚀 Performance improvement (optimization)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
  • 🔨 Refactoring (no logic changes)
  • 🔧 Build/CI or Infrastructure changes
  • 📝 Documentation only

Description

What

  • Add SumAggregateSparkInt64SubOp for Spark non-decimal sum(bigint) → bigint (default via registerSum unless env opts out).
  • Unified updateBatch for addRawInput / addIntermediateResults.
  • sumInt64SubOpCanUseSveKernel(): Linux aarch64 + HWCAP_SVE + svcntb() == 32 (256-bit SVE only); evaluated before decode.
  • Batch glue updateGroupsFromDecodedsveHashAggBatchUpdateGroupSums (SVE kernel, aarch64-only TU, -march=armv8-a+sve).
  • sveAccumulateFlaggedRows: batch accumulation for flat, constant, and dictionary-encoded values.
  • Extend DecodedVector: hashAggNullsLayoutMode / hashAggIndicesLayoutMode (mode1/mode2 — orthogonal null vs value layout) + hashAggMutable* buffer accessors for batch glue.

Disable SubOp (BOLT_SPARK_SUM_INT64_USE_SUBOP)

Env value Factory aggregate
unset, empty, or any value other than the disable forms below SumAggregateSparkInt64SubOp (default)
0 SumAggregate<int64_t, int64_t, int64_t> (SumAggregateBase)
false, no, or off (ASCII, case-insensitive) same as 0

Executor example (Gluten / Spark): spark.executorEnv.BOLT_SPARK_SUM_INT64_USE_SUBOP=0.

Registration

registerSum (BIGINT, non-decimal)
  └─ sparkSumInt64UseSubOpFromEnv() ?
        ├─ true  (default: unset / empty env) → SumAggregateSparkInt64SubOp
        └─ false (0 / false / no / off)     → SumAggregate<int64_t,…>

Runtime call chain (updateBatch; raw / intermediate symmetric)

  1. mayPushdown && top-level Lazy → Base (pushdown, same as SumAggregateBase::updateInternal)
  2. Else Overflow? No → Base
  3. Else sumInt64SubOpCanUseSveKernel()? No → Base (no decode; covers non-Linux aarch64, no SVE, or non-256-bit SVE VL)
  4. Else decode; if decoded input is indirect lazy (e.g. Dictionary(LazyVector)) → load via callable hook, return
  5. Else updateGroupsFromDecodedsveHashAggBatchUpdateGroupSums, return

SVE kernel supports nullable and non-nullable input batches.

Flowchart (mermaid — click to expand)
flowchart TD
  A[updateBatch] --> B{top-level Lazy+ pushdown?}
  B -->|yes| C[SumAggregateBase]
  B -->|no| D{Overflow?}
  D -->|no| C
  D -->|yes| E{canUseSveKernel?}
  E -->|no| C
  E -->|yes| F[decode]
  F --> G{indirect lazy?base == LAZY}
  G -->|yes| H[SimpleCallableHook+ LazyVector::load]
  G -->|no| I[updateGroupsFromDecoded→ Sve.cpp]
Loading

Behavior summary

Topic Shipped behavior
Default aggregate SumAggregateSparkInt64SubOp for non-decimal spark_sum(bigint) unless env opts out
SVE activation Spark overflow checking + Linux aarch64 + 256-bit SVE (svcntb()==32)
Encodings Flat, constant, and dictionary inputs
Input nulls Nullable and non-nullable batches
Lazy Top-level lazy → Base pushdown; indirect lazy (e.g. Dictionary(Lazy)) supported after decode
Fallback Non-aarch64, no SVE, or non-256-bit SVE → SumAggregateBase without decode

Source files and SVE path

File roles — register + SubOp + Base (mermaid — click to expand)
flowchart LR
  R["SumAggregate.cpp"]
  H["SubOp.h"]
  CPP["SubOp.cpp"]
  DV["DecodedVector"]
  Hook["SimpleCallableHook"]
  SVE["Sve.cpp"]
  Base["SumAggregateBase"]

  R -->|"registerSum"| H
  H --> CPP
  CPP -->|"decode"| DV
  DV -->|"indirect lazy"| Hook
  DV -->|"flat/const/dict"| SVE
  CPP -->|"pre-decode fallback"| Base

  classDef prNew fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#000
  classDef prModified fill:#fff3cd,stroke:#ffc107,stroke-width:2px,color:#000
  classDef existing fill:#f6f8fa,stroke:#6c757d,color:#000
  class H,CPP,SVE prNew
  class R,DV prModified
  class Base,Hook existing
Loading

Legend: green = new (SubOp.h / SubOp.cpp / Sve.cpp); yellow = modified (SumAggregate.cpp, DecodedVector); gray = existing (SumAggregateBase, SimpleCallableHook).

After decode (two branches from DecodedVector):

  1. Indirect lazySimpleCallableHook + LazyVector::load (scalar; not Base, not SVE)
  2. Flat / constant / dictionaryupdateGroupsFromDecodedSve.cpp

Pre-decode fallback (steps 1–2, or !canUseSveKernel): delegateToBase()SumAggregateBase (no decode).

Step File / symbol Role
0 SumAggregate.cpp registerSum factory: env check → SumAggregateSparkInt64SubOp or SumAggregateBase for Spark non-decimal BIGINT.
1 SumAggregateSparkInt64SubOp.h SubOp class: addRawInput / addIntermediateResultsupdateBatch; declares sumInt64SubOpCanUseSveKernel() and updateGroupsFromDecoded (all platforms).
2 SumAggregateSparkInt64SubOp.cpp updateBatch dispatch (raw / intermediate symmetric via delegateToBase()): top-level lazy or !Overflow or !canUseSveKernelBase without decode; else decode → indirect lazy hook or updateGroupsFromDecoded. Non-aarch64 stub for glue returns false.
3 DecodedVector hashAggNullsLayoutMode (mode1 — input null layout) + hashAggIndicesLayoutMode (mode2 — value indexing); tags are orthogonal. hashAggMutable*nulls_ / data_ / indices_. SVE glue reads mode1 for per-lane null mask, mode2 for value lookup. See mode table below.
4 updateGroupsFromDecoded Batch glue (member): implemented in SumAggregateSparkInt64SubOpSve.cpp on aarch64. Reads hashAgg* + row mask; calls sveHashAggBatchUpdateGroupSums. Returns true when the SVE kernel handled the batch.
5 sveHashAggBatchUpdateGroupSums · sveAccumulateFlaggedRows SVE kernel: 32-wide chunks; flat, constant, and dictionary encodings; nullable and non-nullable inputs. aarch64-only TU (-march=armv8-a+sve); requires 256-bit SVE at runtime.
DecodedVector mode1 / mode2 (click to expand)

mode1 and mode2 are independent — not a single flat/const/dict enum.

mode1 hashAggNullsLayoutMode — how to read input nulls mode2 hashAggIndicesLayoutMode — how to read values
0 = no null bitmap · 1 = top-level row (nulls_[row]) · 2 = constant null (nulls_[0]) · 3 = via indices (nulls_[indices[row]]) 1 = flat / identity (value[row]) · 2 = constant (value[0]) · 3 = dictionary (value[indices[row]])

Typical shapes (DecodedVectorTest.hashAggLayoutModes):

Input mode1 mode2
non-null flat 0 1
nullable flat 1 1
non-null constant 0 2
null constant 2 2
non-null dictionary 0 3
dictionary + merged top-level nulls 1 3

Buffer accessors (what SVE glue reads after decode):

API Points to When used
hashAggMutableCombinedNullBits() nulls_ bit-packed null bitmap (may be nullptr) mode1 selects nulls_[row], nulls_[0], or nulls_[indices[row]]; mode1==0 → no bitmap
hashAggMutableRawData() data_ — scalar int64 payload (value[]) Always; mode2 selects value[row], value[0], or value[indices[row]]
hashAggMutableIndices() indices_ mode2==3 (dictionary) only

Tests (11 cases added for this PR; names below are gtest suffixes)

Binary Cases
bolt_functions_spark_aggregates_test DuckDB parity: sumInt64SubOpParity, sumInt64SubOpEnvOffParity, sumInt64SubOpNullableSveGate
same SubOp vs Base (expectSparkSumInt64SubOpMatchesBase): SveMatchesBase, NullConstMatchesBase, NonNullFlatMatchesBase, NonNullConstMatchesBase, DictionaryMatchesBase, NumNullsZeroNullableInputMatchesBase, IntermediateDictionaryMatchesBase
bolt_vector_test DecodedVectorTest.hashAggLayoutModes

Other SumAggregationTest cases in the same binary (e.g. overflow, decimalSum) are pre-existing, not part of this PR.

cmake --build --preset conan-release --target bolt_functions_spark_aggregates_test bolt_vector_test

ctest --test-dir _build/Release -R '^bolt_functions_spark_aggregates_test$' --output-on-failure -V -- \
  --gtest_filter='SumAggregationTest.sumInt64SubOp*'

ctest --test-dir _build/Release -R '^bolt_vector_test$' --output-on-failure -V -- \
  --gtest_filter='DecodedVectorTest.hashAggLayoutModes'

Pass --gtest_filter after -- so ctest runs only SubOp-related cases (without it, the aggregate binary runs all registered tests).

Note: sumInt64SubOpEnvOffParity skips on Windows (POSIX setenv). SVE instruction coverage requires Linux aarch64 with 256-bit SVE (svcntb()==32); otherwise SubOp falls back to Base and tests still pass.

Performance Impact

  • No Impact
  • Positive Impact: SVE batch path on flat, constant, dictionary, and typical HashAgg batch shapes; Base fallback skips decode when 256-bit SVE is unavailable. Full TPC-DS 1T self-test: results match baseline; suite-wide E2E gain is limited because int64 sum is a small workload share. Benchmark Results (expand below): TPC-DS 1T HashAgg micro-query on store_sales (group-by + multiple sum(bigint) / decimal-unscaled sums; qtest×5) — ~6.3% mean E2E wall-time vs baseline where sum updates dominate.
Click to view Benchmark Results

TPC-DS 1T (scale 1000) — full suite (self-tested): Numeric results match baseline on native Gluten bundle. End-to-end performance improvement across the full benchmark is not significant — int64 sum / SumAggregateSparkInt64SubOp work is a small fraction of total query time on typical TPC-DS queries. Numbers below are a HashAgg-heavy micro-query only (qtest), to demonstrate the optimized path where sum updates dominate.

Environment (micro-benchmark)

Item Value
Platform Linux aarch64 (async-profiler 4.3 linux-arm64)
Stack Spark 3.4.4, JDK 17, Gluten Bolt columnar backend
Dataset TPC-DS scale 1000 (tpcds_bin_partitioned_varchar_parquet_1000)
Baseline JAR Baseline Gluten bundle (no SubOp)
PR JAR This PR's Gluten bundle (SumAggregateSparkInt64SubOp default)
Executor env BOLT_SPARK_SUM_INT64_USE_SUBOP=1 (baseline JAR has no SubOp; PR SubOp is also default-on when unset)

Workload (same SQL run 5 times — qtestqtest-5 — for stability):

SELECT
  ss_item_sk AS item_sk,
  d_date AS solddate,
  count(*) AS cnt,
  sum(cast(ss_quantity AS bigint)) AS sum_qty,
  sum(ss_ext_sales_price) AS sum_ext_sales,
  sum(ss_ext_discount_amt) AS sum_ext_discount,
  sum(ss_net_paid) AS sum_net_paid,
  sum(ss_net_paid_inc_tax) AS sum_net_paid_tax,
  sum(ss_net_paid_inc_tax - ss_net_paid) AS sum_tax,
  sum(ss_ext_wholesale_cost) AS sum_cost,
  sum(ss_ext_list_price) AS sum_ext_list_price,
  sum(ss_net_profit) AS sum_profit
FROM store_sales, date_dim
WHERE ss_sold_date_sk = d_date_sk
GROUP BY ss_item_sk, d_date
ORDER BY sum_profit DESC, ss_item_sk, d_date
LIMIT 100;

Note: sum(cast(ss_quantity AS bigint)) is an explicit bigint sum. Other sum(decimal(7,2)) columns (ss_ext_sales_price, …) are rewritten by Spark DecimalAggregates to MakeDecimal(sum(UnscaledValue(col)), …) — HashAgg still runs sum(bigint), so they use the same SumAggregateSparkInt64SubOp / SumAggregateBase path (not DecimalSumAggregate when p+10≤18). E2E gain in this micro-query reflects all int64 sum updates in the HashAgg, not sum_qty alone.

Correctness (micro-query): Query output matches baseline on all 5 runs (qtestqtest-5; sorted diff, row-order agnostic).

End-to-end wall time (micro-query; spark-sql seconds; lower is better):

Run baseline PR (SubOp) Δ (s) Faster
qtest 34.396 31.262 −3.134 9.1%
qtest-2 33.443 31.364 −2.079 6.2%
qtest-3 33.134 30.997 −2.137 6.4%
qtest-4 32.519 31.607 −0.912 2.8%
qtest-5 33.857 31.656 −2.201 6.5%
mean 33.470 31.377 −2.093 6.3%

Flame-graph attribution (async-profiler E2E; matched % of samples):

Matched symbols baseline PR (SubOp)
SumAggregateBase addRawInput / addIntermediateResults / updateInternal ~20% (19.3–21.9% per run) 0%
SumAggregateSparkInt64SubOp / SVE kernel (sveHashAgg*, updateGroupsFromDecoded) 0% ~17% (16.2–18.4% per run)

Fireflames see fireflame.zip.

PR replaces per-group scalar SumAggregateBase sum(bigint) updates with the SVE batch path; E2E time drops ~6% on this HashAgg-heavy micro-query.

  • Negative Impact

Release Note

Release Note:

Release Note:
- Spark sum(bigint) on HashAgg: AArch64 SVE batch updates via SumAggregateSparkInt64SubOp (default on).
- SVE batch path: flat, constant, and dictionary inputs when overflow checking is enabled.
- Requires Linux aarch64 + 256-bit SVE (svcntb()==32); otherwise SumAggregateBase.
- Disable SubOp (restore `SumAggregateBase`): `spark.executorEnv.BOLT_SPARK_SUM_INT64_USE_SUBOP=0` (or `false` / `no` / `off`).

Checklist (For Author)

  • I have added/updated unit tests (ctest).
  • I have verified the code with local build (Release/Debug) on Linux aarch64 + SVE (Gluten Bolt bundle; see Benchmark Results).
  • I have run clang-format / linters.
  • (Optional) I have run Sanitizers (ASAN/TSAN) locally for complex C++ changes.
  • TPC-DS 1T on Gluten/Bolt bundle — full suite query output matches baseline (self-tested); HashAgg micro-query qtest run 5× with same check (5/5) + perf/flame in Benchmark Results.

Breaking Changes

  • No
  • Yes (Description: ...)

@CLAassistant

CLAassistant commented May 17, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@LeiRui LeiRui changed the title WIP: feat(sum-subop): Spark BIGINT sum AArch64 SVE on HashAgg feat(sum-subop): Spark BIGINT sum AArch64 SVE on HashAgg May 18, 2026
@LeiRui LeiRui force-pushed the pr-sumInt64 branch 2 times, most recently from 9cab253 to d0e469f Compare May 18, 2026 06:21
@LeiRui LeiRui force-pushed the pr-sumInt64 branch 2 times, most recently from 6726afa to ddddbd4 Compare June 22, 2026 07:14
Add SumAggregateSparkInt64SubOp (adapter updateGroupsFromDecoded, SVE kernel
sveHashAggBatchUpdateGroupSums), DecodedVector hashAgg* layout APIs, env kill
switch BOLT_SPARK_SUM_INT64_USE_SUBOP, and unit tests (DuckDB parity, env-off
parity, SubOp vs Base, nullable gate, null constant, hashAgg layout modes).

Co-authored-by: Old-Li883 <lichenhao9@huawei.com>
Co-authored-by: helloxteen <zhangxin440@h-partners.com>
LeiRui added 2 commits July 5, 2026 23:06
- Drop numNulls_/mayHaveNulls outer gates; unify updateBatch for raw/intermediate
- Remove mode2==2 early exit; use value[0] for constant mapping in SVE kernel
- Add NonNullFlat/NonNullConst SubOp-vs-Base parity tests
- Cache sumInt64SubOpCanUseSveKernel() (HWCAP_SVE + svcntb()==32) on Linux aarch64
- Check canUseSveKernel before decode; fallback to SumAggregateBase without decode
- Hoist mode2 constant (value[0]) across 32-row SVE blocks
- Deduplicate tests via expectSparkSumInt64SubOpMatchesBase; add dictionary/nullable cases
…llback

- Remove mayPushdown/numNulls_/isDecimal gates on LAZY decode hook path

- Consolidate Base fallback via delegateToBase helper
@LeiRui

LeiRui commented Jul 5, 2026

Copy link
Copy Markdown
Author

Force-pushed uncouple after rebasing onto latest upstream main @ f12be06. Branch tip: 8286363.

Rebase / history

  • Rebased onto current main; branch is 4 commits on top of main:
    1. feat(sum-subop): Spark BIGINT sum AArch64 SVE on HashAgg
    2. fix(sum-subop): broaden SVE eligibility and fix constant sum path
    3. perf(sum-subop): cache SVE probe, skip decode when ineligible
    4. refactor(sum-subop): align indirect lazy path and consolidate Base fallback
  • Diff vs main: 8 files, +1160 / −1 (SumAggregateSparkInt64SubOpDecodedVector hashAgg APIs, tests).

Behavior summary (this PR)

  • SumAggregateSparkInt64SubOp is the default for non-decimal spark_sum(bigint) → bigint. Disable with BOLT_SPARK_SUM_INT64_USE_SUBOP=0 (or false / no / off) to restore SumAggregateBase.
  • SVE batch path for flat, constant, and dictionary inputs when Spark overflow checking is on and Linux aarch64 has 256-bit SVE (svcntb()==32).
  • Lazy inputs: top-level lazy + pushdown → SumAggregateBase; indirect lazy after decode (e.g. Dictionary(Lazy)) → SimpleCallableHook + load (not SVE).
  • Fallback: non-aarch64 or non-256-bit SVE → SumAggregateBase without decode in SubOp.

PR description

Updated to match the above (tests, disable env, benchmark notes). Please review the latest description.

Testing

  • Unit: SumAggregationTest.sumInt64SubOp*DecodedVectorTest.hashAggLayoutModes (ctest commands in PR body).
  • TPC-DS 1T (scale 1000): query output matches baseline (self-tested). Suite-wide E2E gain is limited (int64 sum is a small workload share); HashAgg micro-query perf/flame details are in the PR body.

Sorry for the noisy diff from the rebase. Please re-review if a previous approval was tied to the pre-rebase diff. Happy to squash or adjust commits before merge if you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants